<template>
  <div
    class="virtual-table-outer"
    :style="{
      height: height + 'px',
      width: width + 'px',
      overflow: 'auto',
      position: 'relative',
    }"
    ref="containerRef"
    @scroll="onScroll"
  >
    <!-- 顶部占位 -->
    <div :style="{ height: topPadding + 'px' }"></div>
    <!-- 主体部分（横向滚动区） -->
    <div style="display: flex">
      <!-- 左侧占位 -->
      <div :style="{ width: leftPadding + 'px', flexShrink: 0 }"></div>
      <!-- 渲染表格 -->
      <table class="virtual-table" style="border-collapse: collapse">
        <thead>
          <tr>
            <th
              v-for="(col, colIdx) in visibleColumns"
              :key="col.key || colIdx"
              :style="{
                minWidth: colWidth + 'px',
                border: '1px solid #eee',
                background: '#f8f9fa',
              }"
            >
              {{ col.title || col.key || colIdx }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr
            v-for="(item, rowIdx) in visibleItems"
            :key="getKey(item, startIdx + rowIdx)"
            :ref="(el) => setItemRef(el, startIdx + rowIdx)"
          >
            <td
              v-for="(col, colIdx) in visibleColumns"
              :key="col.key || colIdx"
              :style="{
                minWidth: colWidth + 'px',
                border: '1px solid #eee',
                fontSize: '13px',
              }"
            >
              <slot
                :item="item"
                :index="startIdx + rowIdx"
                :col="col"
                :rowIndex="startIdx + rowIdx"
                :colIndex="colStartIdx + colIdx"
                name="cell"
              >
                {{ item[col.key] ?? "" }}
              </slot>
            </td>
          </tr>
        </tbody>
      </table>
      <!-- 右侧占位 -->
      <div :style="{ width: rightPadding + 'px', flexShrink: 0 }"></div>
    </div>
    <!-- 底部占位 -->
    <div :style="{ height: bottomPadding + 'px' }"></div>
  </div>
</template>

<script setup>
import { ref, computed, watch, reactive, nextTick } from "vue";

const props = defineProps({
  list: { type: Array, required: true }, // 行数据
  columns: { type: Array, required: true }, // 列配置
  itemHeight: { type: Number, required: false, default: 40 }, // 默认行高估值
  colWidth: { type: Number, default: 120 }, // 列宽
  width: { type: Number, default: 800 },
  height: { type: Number, default: 400 },
  buffer: { type: Number, default: 6 }, // 纵向缓冲
  colBuffer: { type: Number, default: 2 }, // 横向缓冲
  getKey: { type: Function, default: (item, idx) => idx },
});

// 纵向参数
const containerRef = ref();
const startIdx = ref(0);
const endIdx = ref(0);
const itemRefs = reactive({}); // 用于记录每个 item 的 ref
const heightMap = reactive({}); // 记录每个 item 的实际高度

// 横向参数
const colStartIdx = ref(0);
const colEndIdx = ref(0);

// 1. 动态高度累计表
const offsetMap = computed(() => {
  let offsets = [0];
  for (let i = 0; i < props.list.length; i++) {
    offsets[i + 1] = offsets[i] + (heightMap[i] || props.itemHeight);
  }
  return offsets;
});

// 2. 当前可见的起始列
function getColStartIndex(scrollLeft) {
  return Math.max(0, Math.floor(scrollLeft / props.colWidth) - props.colBuffer);
}
const visibleColCount = computed(
  () => Math.ceil(props.width / props.colWidth) + props.colBuffer * 2
);
const visibleColumns = computed(() => {
  const total = props.columns.length;
  const cStart = colStartIdx.value;
  const cEnd = Math.min(cStart + visibleColCount.value, total);
  colEndIdx.value = cEnd;
  return props.columns.slice(cStart, cEnd);
});
const leftPadding = computed(() => colStartIdx.value * props.colWidth);
const rightPadding = computed(
  () => (props.columns.length - colEndIdx.value) * props.colWidth
);

// 1. 计算累计高度数组
// const offsetMap = computed(() => {
//   let offsets = [0];
//   for (let i = 0; i < props.list.length; i++) {
//     offsets[i + 1] = offsets[i] + (heightMap[i] || props.estimateHeight);
//   }
//   return offsets;
// });

// 2. 计算当前 scrollTop 应该显示的第几个 item
function getStartIndex(scrollTop) {
  // 二分查找 offsetMap 中第一个 >= scrollTop 的下标
  let low = 0,
    high = offsetMap.value.length - 1;
  while (low < high) {
    let mid = (low + high) >> 1;
    if (offsetMap.value[mid] < scrollTop) low = mid + 1;
    else high = mid;
  }
  return Math.max(0, low - props.buffer);
}

// 3. 渲染区
const visibleItems = computed(() => {
  const _startIdx = startIdx.value;
  const viewBottom = (containerRef.value?.scrollTop || 0) + props.height;
  // 用累计高度找出最大能展示到第几个 item
  let _endIdx = _startIdx;
  while (_endIdx < props.list.length && offsetMap.value[_endIdx] < viewBottom) {
    _endIdx++;
  }
  endIdx.value = Math.min(_endIdx + props.buffer, props.list.length);
  return props.list.slice(_startIdx, Math.min(props.list.length, endIdx.value));
});

// 4. 顶部/底部占位
const topPadding = computed(() => offsetMap.value[startIdx.value] || 0);
const bottomPadding = computed(
  () =>
    offsetMap.value[props.list.length] - (offsetMap.value[endIdx.value] || 0)
);

// 5. 记录每个 item 的 ref 并在下次渲染时测量实际高度
function setItemRef(el, idx) {
  if (el) {
    itemRefs[idx] = el;
    // nextTick 保证 DOM 已经渲染
    nextTick(() => {
      const rect = el.getBoundingClientRect();
      if (rect.height && heightMap[idx] !== rect.height) {
        //仅当高度变化时才更新
        heightMap[idx] = rect.height;
      }
    });
  }
}

// 6. 滚动时重算 startIdx
let ticking = false;
function onScroll() {
  if (!containerRef.value) return;
  if (!ticking) {
    //如果当前没有pending的UI更新，就安排一次requestAnimationFrame，记录ticking为true。
    window.requestAnimationFrame(() => {
      const scrollTop = containerRef.value.scrollTop;
      const scrollLeft = containerRef.value.scrollLeft;
      startIdx.value = getStartIndex(scrollTop);
      colStartIdx.value = getColStartIndex(scrollLeft);
      ticking = false;
    });
    ticking = true;
  }
}

// 首次和数据变化时初始化
watch(
  () => props.list,
  () => {
    startIdx.value = 0;
    Object.keys(heightMap).forEach((k) => delete heightMap[k]);
  }
);
</script>

<style scoped>
.virtual-table-outer {
  border: 1px solid #eee;
  background: #fafbfc;
}
.virtual-table th,
.virtual-table td {
  padding: 4px 8px;
}
</style>
